/* -*- mode: c++; coding: utf-8-unix -*-
 */

#include <sysexits.h>
#include <glib.h>
#include <stdio.h>
#include <string.h>
#include <assert.h>
#include <map>

#include "Configuration.h"
#include "Log.h"
#include "mkstr"
#include "autofree"

typedef const char * CSTR;
typedef CSTR const CSTR_C;

using namespace std;
using namespace cfg;

typedef autofree<gchar, gfree>  auto_gchar;
typedef map<ChannelSpec, string> output_map;

CSTR_C MODE_NAMES[] = { "Unspecified", "DFT", "DCT" };
CSTR_C OUTPUTTYPE_NAMES[] = { "Unspecified", "Grayscale", "Merged" };

static CSTR_C O_SUMMARY = "- Discrete Fourier and Cosine Transformation";

struct Config::PImp
{
	Mode mode;
	OutputType outputType;
	channel_list channels;
	string infile;
	string singleOutfile;
	output_map outfile;
	size_t threads;
	size_t framesize;
	size_t outw, outh;
	double squeeze;
	bool center;

	PImp() : mode(UnspecifiedMode), outputType(UnspecifiedType) {}

	void ac(const string &name, Channel channel, Subchannel subchannel = Real)
	{
		ChannelSpec cs = { name, channel, subchannel };
		channels.push_back(cs);
	}

	void parseMode(CSTR _mode)
	{
		if (!_mode) throw string("Option `mode' must be specified.");

		if (!strncmp(_mode, "dft", 4)) mode = DFT;
		else if (!strncmp(_mode, "dct", 4)) mode = DCT;
		else throw string(MKStr() << "Unknown mode: " << _mode);
	}
	void parseOuttype(CSTR _outputType)
	{
		if (!_outputType)
			outputType = Grayscale;
		else if (!strncmp(_outputType,"gray",5)||!strncmp(_outputType,"grey",5))
			outputType = Grayscale;
		else if (!strncmp(_outputType, "merged", 7))
			outputType = Merged;
		else
			throw string(MKStr() << "Unknown outtype: " << _outputType);
	}
	void parseChannels(char** _channels)
	{
		assert(mode);
		if (!_channels)
			throw string("At least one output channel must be specified.");
		if (mode == DFT) parseDFTChannels(_channels);
		else             parseDCTChannels(_channels);
	}
	void parseDFTChannels(char** _channels)
	{
		for (char** i = _channels; *i; i++)
		{
			if (!strncmp(*i, "rr", 3)) ac(*i, Red, Real);
			else if (!strncmp(*i, "ri", 3)) ac(*i, Red, Imaginary);
			else if (!strncmp(*i, "gr", 3)) ac(*i, Green, Real);
			else if (!strncmp(*i, "gi", 3)) ac(*i, Green, Imaginary);
			else if (!strncmp(*i, "br", 3)) ac(*i, Blue, Real);
			else if (!strncmp(*i, "bi", 3)) ac(*i, Blue, Imaginary);
			else if (!strncmp(*i, "xr", 3)) ac(*i, Gray, Real);
			else if (!strncmp(*i, "xi", 3)) ac(*i, Gray, Imaginary);
			else if (!strncmp(*i, "rm", 3)) ac(*i, Red, Magnitude);
			else if (!strncmp(*i, "rp", 3)) ac(*i, Red, Phase);
			else if (!strncmp(*i, "gm", 3)) ac(*i, Green, Magnitude);
			else if (!strncmp(*i, "gp", 3)) ac(*i, Green, Phase);
			else if (!strncmp(*i, "bm", 3)) ac(*i, Blue, Magnitude);
			else if (!strncmp(*i, "bp", 3)) ac(*i, Blue, Phase);
			else if (!strncmp(*i, "xm", 3)) ac(*i, Gray, Magnitude);
			else if (!strncmp(*i, "xp", 3)) ac(*i, Gray, Phase);
			
			else throw string(MKStr() << "Invalid channel spec: " << *i);
		}
	}
	void parseDCTChannels(char** _channels)
	{
		for (char** i = _channels; *i; i++)
		{
			if (!strncmp(*i, "r", 2)) ac(*i, Red);
			else if (!strncmp(*i, "g", 2)) ac(*i, Green);
			else if (!strncmp(*i, "b", 2)) ac(*i, Blue);
			else if (!strncmp(*i, "x", 2)) ac(*i, Gray);
			else throw string(MKStr() << "Invalid channel spec: " << *i);
		}
	}
	void parseInfile(CSTR _infile)
	{
		if (!_infile) throw string("Infile must be specified.");
		infile = _infile;
	}
	void parseOutfile(CSTR _outfile)
	{
		assert(outputType);
		assert(!channels.empty());

		if (!_outfile) throw string("Outfile must be specified");

		if (outputType == Merged)
			singleOutfile = _outfile;
		else
		{
			auto_gchar dirname(g_path_get_dirname(_outfile));
			auto_gchar basename(g_path_get_basename(_outfile));
			USELOG();

			for (chlist_citerator i = channels.begin();
			     i != channels.end(); i++)
			{
				const string &bn =
					MKStr() << i->name << '-' << basename.get();
				auto_gchar fn(
					g_build_filename(dirname.get(), bn.c_str(), NULL));
				outfile[*i] = string(fn.get());
			}
		}
	}
};

Config::Config(int argc, char **argv)
{
	_pimp = new PImp();
	try
	{
		char *infile = 0, *outfile = 0, *mode = 0,
			*outtype = 0, **channels = 0;
		int th = 4, framesize = 100;
		int ow = 0, oh = 0;
		double sq = 1;
		int cent = 1;
		GOptionEntry options[] = {
			{ "threads", 0, 0, G_OPTION_ARG_INT, &th,
			  "Number of maximum concurrent threads.", "NUM" },
			{ "frame", 0, 0, G_OPTION_ARG_INT, &framesize,
			  "Maximum number of lines processed by a thread.", "NUM" },
			{ "mode", 'm', 0, G_OPTION_ARG_STRING, &mode,
			  "Algorithm to perform. MODE <- {dft,dct}", "MODE" },
			{ "outtype", 't', 0, G_OPTION_ARG_STRING, &outtype,
			  "Output type. TYPE <- {gray,grey,merged}", "TYPE"},
			{ "width", 'w', 0, G_OPTION_ARG_INT, &ow,
			  "Output width (max frequency)", "NUM" },
			{ "height", 'h', 0, G_OPTION_ARG_INT, &oh,
			  "Output height (max frequency)", "NUM" },
			{ "squeeze", 's', 0, G_OPTION_ARG_DOUBLE, &sq,
			  "Squeeze factor of output values. REAL>0", "REAL" },
			{ "center", 's', 0, G_OPTION_ARG_INT, &cent,
			  "Center result image on image mean.", "1/0" },
			{ "infile", 'i', 0, G_OPTION_ARG_FILENAME, &infile,
			  "Input file path", NULL },
			{ "outfile", 'o', 0, G_OPTION_ARG_FILENAME, &outfile,
			  "Output path. If --type=merged, used as filename. If multiple output files are generated, used as filename template.", NULL },
			{ "channel", 'c', 0, G_OPTION_ARG_STRING_ARRAY, &channels,
			  "Channels to save. This option can be specified more than once to select multiple channels to be saved.\n\tIf --outtype=merged, exactly three distinct channels must be specified, which to be mapped to the red, green and blue channels of the merged image in order.\n\tIf --outtype!=merged, channels specified multiple times are ignored.\n\tDepending on MODE:\n\tMODE=dft => CHSPEC <- {r,g,b,x}X{r,i,m,p}\n\tMODE=dct => CHSPEC <- {r,g,b,x}\n\tChannel names stand for Red, Green, Blue, x=Intensity; For DFT: Real, Imaginary, Magnitude, Phase. E.g.: -c gm select the megnitude result of transforming the green channel.", "CHSPEC" },
			{ NULL }
		};

		autofree<GOptionContext, g_option_context_free>
			context(g_option_context_new(O_SUMMARY));
		GError *error = NULL;
		g_option_context_add_main_entries(context, options, 0);

		try
		{
			if (!g_option_context_parse(context, &argc, &argv, &error))
				throw EX_USAGE;
			_pimp->parseMode(mode);
			_pimp->parseOuttype(outtype);
			_pimp->parseChannels(channels);
			_pimp->parseInfile(infile);
			_pimp->parseOutfile(outfile);
			_pimp->threads=th;
			_pimp->framesize=framesize;
			_pimp->outw = ow;
			_pimp->outh = oh;
			_pimp->center = cent;

			if (sq <= 0) throw string("Invalid (<=0) value for --squeeze.");
			_pimp->squeeze = sq;

			if (outputType() == Merged)
			{
				channel_list lst(_pimp->channels);
				lst.sort();
				lst.unique();

				if (_pimp->channels.size() != 3
				    || lst.size() != _pimp->channels.size())
				{
					throw string("If --outtype is merged, exactly "
						     "three distinct channels must be "
						     "specified.");
				}
			}
			if (outputType() != Merged)
			{
				_pimp->channels.sort();
				_pimp->channels.unique();
			}
		}
		catch (...)
		{
			fprintf(stderr, "%s",
				g_option_context_get_help(context,false, NULL));
			throw;
		}
	}
	catch (...)
	{
		delete _pimp;
		throw;
	}
}
Config::~Config()
{
	delete _pimp;
}

Mode Config::mode() const { return _pimp->mode; }
OutputType Config::outputType() const { return _pimp->outputType; }
const channel_list& Config::selectedChannels() const { return _pimp->channels; }
const string& Config::infile() const { return _pimp->infile; }
const string& Config::outfile() const
{
	if (outputType() != Merged)
		throw string("Invalid operation: outfile() when mode!=Merged");
	return _pimp->singleOutfile;
}
const string& Config::outfile(ChannelSpec ch) const
{
	if (outputType() == Merged)
		throw string("Invalid operation: outfile(ChannelSpec)"
			     " when mode=Merged");
	return _pimp->outfile[ch];
}
size_t Config::threadcount() const { return _pimp->threads; }
size_t Config::framesize() const { return _pimp->framesize; }
size_t Config::outWidth() const { return _pimp->outw; }
size_t Config::outHeight() const { return _pimp->outh; }
double Config::squeeze() const { return _pimp->squeeze; }
bool Config::center() const { return _pimp->center; }

bool cfg::operator<(const ChannelSpec &one,
		    const ChannelSpec &other)
{
	if (&one == &other || one.channel > other.channel)
		return false;
	else if (one.channel < other.channel)
		return true;
	else
		return one.subchannel < other.subchannel;
}
bool cfg::operator==(const ChannelSpec &one,
		     const ChannelSpec &other)
{
	if (&one == &other)
		return true;
	else
		return (one.channel == other.channel
			&& one.subchannel == other.subchannel);
}

string Config::toString() const
{
	MKStr buf;
	buf << "Threads:     " << threadcount() << endl
	    << "Framesize:   " << framesize() << endl
	    << "Mode:        " << MODE_NAMES[mode()] << endl
	    << "Output type: " << OUTPUTTYPE_NAMES[outputType()] << endl
	    << "Output size: " << outWidth() << 'x' << outHeight() << endl
	    << "Squeeze:     " << squeeze() << endl
	    << "In file:     '" << infile() << "'" << endl
	    << "Channels and output files:" << endl;
	if (outputType() == Merged)
	{
		bool first = true;
		for (chlist_citerator i = selectedChannels().begin();
		     i != selectedChannels().end(); i++)
		{
			if (first) first = false;
			else buf << "+";
			buf << i->name;
		}

		buf << " -> '" << outfile() << '\'' << endl;
	}
	else
	{
		typedef output_map::const_iterator it;
		for (chlist_citerator i = selectedChannels().begin();
		     i != selectedChannels().end(); i++)
		{
			buf << i->name << " -> '" << outfile(*i) << '\'' << endl;
		}
	}

	return buf;
}
